package com.opennews.openplatform

import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.Transactional
import groovy.transform.CompileDynamic
import org.hibernate.criterion.CriteriaSpecification

@Transactional
@GrailsCompileStatic
class IncomeExpenseDetailTypeService {
    IncomeExpenseDetailService incomeExpenseDetailService

    /**
     * Prepares the initial data for IncomeExpenseDetailType.
     */
    void init(String accountGroupId) {
        if (IncomeExpenseDetailType.countByAccountGroup(AccountGroup.proxy(accountGroupId)) == 0) {
            // Prepares the default expense types based on predefined base types.
            IncomeExpenseDetailBaseType.all.each {
                new IncomeExpenseDetailType(it.properties).with {
                    accountGroup = AccountGroup.proxy(accountGroupId)
                    save()
                }
            }
        }
    }

    /**
     * Queries all detail types which associated with provided account group.
     * @param accountGroupId : The string of the user account group id.
     * @param incomeExpenseType: The string of income/expense type.
     * @return List of map instance which contains keys below.
     *          id: String of detail type id.
     *          incomeExpenseType: String of income/expense type.
     *          name: String of detail type name.
     *          title: String of detail type title.
     *          subtitle: String of detail type subtitle.
     *          analysisIncluded: Bool to indicate if this is included for analysis.
     *          dateCreated: The formatted created date.
     *          lastUpdated: The formatted last updated date.
     */
    @Transactional(readOnly = true)
    @CompileDynamic
    List<Map<String, ?>> queryByAccountGroup(String accountGroupId, String incomeExpenseType) {
        return IncomeExpenseDetailType.createCriteria().list {
            // Transforms the result data format to Map instance.
            resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)

            // Sets the query condition.
            and {
                eq("accountGroup.id", accountGroupId)

                if (!SharedUtil.isNullOrEmpty(incomeExpenseType)) {
                    eq("incomeExpenseType", incomeExpenseType)
                }
            }

            // Specifies the property we need.
            projections {
                property("id", "id")
                property("incomeExpenseType", "incomeExpenseType")
                property("name", "name")
                property("title", "title")
                property("subtitle", "subtitle")
                property("analysisIncluded", "analysisIncluded")
                property("dateCreated", "dateCreated")
                property("lastUpdated", "lastUpdated")
            }

            order("orderIndex", "asc")
        } as List<Map<String, ?>>
    }

    /**
     * Queries all detail types which associated with provided account group.
     * @param accountGroupId: The string of the user's account group id.
     * @param incomeExpenseType: The string of income/expense type.
     * @return List of name string.
     */
    @Transactional(readOnly = true)
    @CompileDynamic
    List<String> queryNameForAnalysisByAccountGroup(String accountGroupId, String incomeExpenseType) {
        return IncomeExpenseDetailType.createCriteria().list {
            // Sets the query condition.
            and {
                eq("accountGroup.id", accountGroupId)
                eq("incomeExpenseType", incomeExpenseType)
                eq("analysisIncluded", true)
            }

            // Specifies the property we need.
            projections {
                property("name", "name")
            }

            order("orderIndex", "asc")
        } as List<String>
    }

    /**
     * Queries the input name list and makes sure they belong to the specific account group.
     * @param inputIncomeExpenseType : The string of the income/expense type.
     * @param names : The string list of name.
     * @param accountGroupId: The string of the user's account group id.
     * @return The matched name list.
     */
    @Transactional(readOnly = true)
    @CompileDynamic
    List<String> queryAccountGroupDetailTypes(String inputIncomeExpenseType, List<String> names, String accountGroupId) {
        if (names.size() > 0) {
            // Queries the names match the income/expense type and account group.
            return IncomeExpenseDetailType.where {
                incomeExpenseType == inputIncomeExpenseType &&
                    accountGroup == AccountGroup.proxy(accountGroupId) &&
                    name in names
            }.projections {
                property("name")
            }.toList() as List<String>
        } else {
            return []
        }
    }

    /**
     * Checks if the detail type name is already existing with the same account id, income/expense type and id.
     * @param accountGroupId: The string of the account group id.
     * @param incomeExpenseType: The string of the income/expense type.
     * @param name: The string of detail type name.
     * @return True means the input detail type name already exists. False means NOT.
     */
    @Transactional(readOnly = true)
    @CompileDynamic
    boolean checkExisting(String accountGroupId, String incomeExpenseType, String id, String name) {
        name = name.toUpperCase()

        return IncomeExpenseDetailType.createCriteria().get {
            and {
                eq("accountGroup.id", accountGroupId)
                eq("incomeExpenseType", incomeExpenseType)
                eq("name", name)

                if (!SharedUtil.isNullOrEmpty(id)) {
                    ne("id", id)
                }
            }

            projections {
                count("id")
            }
        } > 0
    }

    /**
     * Queries the current max order index with provided account group id and income/expense type.
     * @param accountGroupId: The string of the account group id.
     * @param incomeExpenseType: The string of the income/expense type.
     * @return The int number of current max order index.
     */
    @Transactional(readOnly = true)
    @CompileDynamic
    int queryMaxOrderIndex(String accountGroupId, String incomeExpenseType) {
        def maxOrderIndex = IncomeExpenseDetailType.createCriteria().get {
            // Sets the query condition.
            and {
                eq("accountGroup.id", accountGroupId)
                eq("incomeExpenseType", incomeExpenseType)
            }

            // Takes the max order index.
            projections {
                max("orderIndex")
            }
        } as Integer

        if (maxOrderIndex == null) {
            maxOrderIndex = 0
        }

        return maxOrderIndex
    }

    /**
     * Queries the next max order index with provided account group id and income/expense type.
     * @param accountGroupId: The string of the account group id.
     * @param incomeExpenseType: The string of the income/expense type.
     * @return The int number of next max order index.
     */
    @Transactional(readOnly = true)
    int queryNextMaxOrderIndex(String accountGroupId, String incomeExpenseType) {
        return queryMaxOrderIndex(accountGroupId, incomeExpenseType) + 1
    }

    /**
     * Queries all detail types which associated with provided account group which is not included by analysis.
     * @param accountGroupId: The string of the user account group id.
     * @param type: The string of IncomeExpenseTypeConstant.
     * @return List of map instance which contains keys below.
     *          name: String of detail type name.
     *          title: String of detail type title.
     */
    @Transactional(readOnly = true)
    @CompileDynamic
    List<Map<String, ?>> queryAnalysisNotIncludedDetailTypes(String accountGroupId, String incomeExpenseType) {
        return IncomeExpenseDetailType.createCriteria().list {
            // Transforms the result data format to Map instance.
            resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)

            and {
                eq("accountGroup.id", accountGroupId)
                eq("incomeExpenseType", incomeExpenseType)
                eq("analysisIncluded", false)
            }

            projections {
                property("name", "name")
                property("title", "title")
            }

            order("orderIndex", "asc")
        } as List<Map<String, ?>>
    }

    /**
     * Resets the detail types order based on the sequence of provided id list.
     * @param accountGroupId: The string of user account group id.
     * @param detailTypeIds: The list of string of detail types.
     */
    @CompileDynamic
    void resetOrder(String accountGroupId, List<String> detailTypeIds) {
        // Updates each order index.
        for (def i = 0; i < detailTypeIds.size(); i++) {
            IncomeExpenseDetailType.where {
                id == detailTypeIds[i] &&
                    accountGroup == AccountGroup.proxy(accountGroupId)
            }.updateAll([orderIndex: i + 1])
        }
    }

    /**
     * Adds new detail type for account group.
     * @param data : The map which contains keys with same name as detail type properties.
     * @param accountGroupId : The string of the user account group id.
     * @return Map instance which contains keys below.
     *          id: String of detail type id.
     *          incomeExpenseType: String of income/expense type.
     *          name: String of detail type name.
     *          title: String of detail type title.
     *          subtitle: String of detail type subtitle.
     *          analysisIncluded: Bool to indicate if this is included for analysis.
     *          dateCreated: The formatted created date.
     *          lastUpdated: The formatted last updated date.
     */
    Map<String, ?> add(Map<String, ?> data, String accountGroupId) {
        Map<String, ?> savedDetailType

        if (!checkExisting(accountGroupId, data.incomeExpenseType.toString(), null, data.name.toString())) {
            // Creates new IncomeExpenseDetailType with posted data.
            def detailType = new IncomeExpenseDetailType(data)
            detailType.name = detailType.name.toUpperCase()
            detailType.accountGroup = AccountGroup.proxy(accountGroupId)
            detailType.orderIndex = queryNextMaxOrderIndex(accountGroupId, detailType.incomeExpenseType)

            // Calling save(flush: true) here in order to getting the values of dateCreated and lastUpdated.
            // All properties' values will be returned to the client to void next round detail query request.
            detailType.save(flush: true)

            savedDetailType = convertToMap(detailType)
        }

        return savedDetailType
    }

    /**
     * Updates detail type based on provided map data and its id.
     * The map keys must match detail type property names.
     * If the map item will be ignored if its value is null.
     * @param data : The map which contains keys with same name as detail type properties.
     * @param accountGroupId : The string of the user account group id.
     * @return Map instance which contains keys below.
     *          id: String of detail type id.
     *          incomeExpenseType: String of income/expense type.
     *          name: String of detail type name.
     *          title: String of detail type title.
     *          subtitle: String of detail type subtitle.
     *          analysisIncluded: Bool to indicate if this is included for analysis.
     *          dateCreated: The formatted created date.
     *          lastUpdated: The formatted last updated date.
     */
    Map<String, ?> update(Map<String, ?> data, String accountGroupId) {
        Map<String, ?> savedDetailType
        if (!checkExisting(accountGroupId, data.incomeExpenseType.toString(), data.id.toString(), data.name.toString()) &&
                IncomeExpenseDetailType.countByAccountGroupAndId(AccountGroup.proxy(accountGroupId), data.id.toString()) == 1) {
            // Updates the detail type.
            def detailType = IncomeExpenseDetailType.proxy(data.id.toString())
            def oldDetailType = detailType.name
            detailType.properties.putAll(data)
            detailType.name = detailType.name.toUpperCase()

            // Calling save(flush: true) here in order to getting the values of dateCreated and lastUpdated.
            // All properties' values will be returned to the client to void next round detail query request.
            detailType.save(flush: true)

            def newDetailType = detailType.name

            // Updates the detail type name which referenced by IncomeExpenseDetail.
            def hql = """
                        UPDATE 
                            IncomeExpenseDetail 
                        SET 
                            detailType = :newDetailType 
                        WHERE 
                            accountGroup.id = :accountGroupId AND 
                            type = :type AND 
                            detailType = :oldDetailType
                      """

            IncomeExpenseDetail.executeUpdate(hql, [
                accountGroupId: accountGroupId,
                newDetailType: newDetailType,
                oldDetailType: oldDetailType,
                type: detailType.incomeExpenseType
            ])

            savedDetailType = convertToMap(detailType)
        }

        return savedDetailType
    }

    /**
     * Deletes the IncomeExpenseDetailTypes with provides list of id.
     * @param inputIncomeExpenseType : The string of the income/expense type.
     * @param inputName : The string of the detail type name.
     * @param accountGroupId : The string of the user account group id.
     * @return True means deleted. False means this detail type has been used and cannot be deleted.
     */
    @CompileDynamic
    Boolean delete(String inputIncomeExpenseType, String inputName, String accountGroupId) {
        def isDeleted = false

        // Checks if this detail type is used by income/expense.
        if (IncomeExpenseDetail.countByTypeAndDetailTypeAndAccountGroup(inputIncomeExpenseType, inputName, AccountGroup.proxy(accountGroupId)) == 0) {
            // If this detail type is not used then deletes it.
            IncomeExpenseDetailType.where {
                incomeExpenseType == inputIncomeExpenseType &&
                    name == inputName &&
                    accountGroup == AccountGroup.proxy(accountGroupId)
            }.deleteAll()

            isDeleted = true
        }

        return isDeleted
    }

    /**
     * Deletes the IncomeExpenseDetailTypes with provides list of name.
     * @param incomeExpenseType : The string of the income/expense type.
     * @param accountGroupId : The string of the user account group id.
     * @param names : The string list of name.
     * @return The list of the id string which have been actually deleted.
     */
    @CompileDynamic
    List<String> delete(String incomeExpenseType, List<String> names, String accountGroupId) {
        List<String> deletedDetailTypeIds = []

        // Queries the input id list and makes sure they belong to the specific account group.
        // This is an example shows how to secure the data by taking extra action.
        // This can void other account group users call api directly and provide these ids which they don't actually own them.
        names = queryAccountGroupDetailTypes(incomeExpenseType, names, accountGroupId)

        // Queries all detail type names which used by income/expense.
        def usedNames = incomeExpenseDetailService.queryUsedDetailTypeNames(incomeExpenseType, names, accountGroupId)

        // Excludes all used detail type names.
        names = names - usedNames

        if (names.size() > 0) {
            // Queries all ids based on incomeExpenseType and names.
            // This is will be returned to client for list item managing.
            deletedDetailTypeIds = IncomeExpenseDetailType.createCriteria().list {
                and {
                    eq("accountGroup.id", accountGroupId)
                    eq("incomeExpenseType", incomeExpenseType)
                    'in'("name", names)
                }

                projections {
                    property("id")
                }
            } as List<String>

            // Executes batch deleting by DetachedCriteria to delete any unused detail type.
            IncomeExpenseDetailType.where {
                id in deletedDetailTypeIds
            }.deleteAll()
        }

        return deletedDetailTypeIds
    }

    /**
     * Converts the input detail type instance to Map instance.
     * @param detailType: The instance of IncomeExpenseDetailType.
     * @return Map instance which contains keys below.
     *          id: String of detail type id.
     *          incomeExpenseType: String of income/expense type.
     *          name: String of detail type name.
     *          title: String of detail type title.
     *          subtitle: String of detail type subtitle.
     *          analysisIncluded: Bool to indicate if this is included for analysis.
     *          dateCreated: The formatted created date.
     *          lastUpdated: The formatted last updated date.
     */
    private Map<String, ?> convertToMap(IncomeExpenseDetailType detailType) {
        return [
            id: detailType.id,
            incomeExpenseType: detailType.incomeExpenseType,
            name: detailType.name,
            title: detailType.title,
            subtitle: detailType.subtitle,
            analysisIncluded: detailType.analysisIncluded,
            dateCreated: detailType.dateCreated,
            lastUpdated: detailType.lastUpdated
        ]
    }
}